開始發文到今天是第十五天,三十天的旅程也將完成前半部。原本打算在三十天內把這十多種 DevOps 的開源工具走過一遍,以目前的進度應該是很難達成了。從每天能寫的量來看,要在三天左右的時間介紹完一個工具還頗有難度,所以剩下的十五天,能寫到那就算那吧,可能把 Docker 相關工具包含 Kubernetes,以及 Jenkins 看完就差不多了。今天開始來看 Docker。
【應試目標能力】
Topic 702 - 容器管理 Container Management (16)
702.1 容器使用 Container Usage (7)
702.2 容器部署及編配 Container Deployment and Orchestration (5)
702.3 容器設施 Container Infrastructure (4)
Docker、Kubernetes 這一系列與容器相關的應用管理在考試中一共有16 分,佔比是四分之一強。Docker 應該還是個在浪頭上的技術,據我所知這一陣子很紅的微服務,也是將應用程式解耦,以容器為基礎運行單元來當作服務的架構。使用容器在維運面帶來一個很重要的好處就是易於擴展,達到更好的資源利用。印象中 Docker 紅起來好像是在四、五年前,那時身旁有在玩 Docker 的朋友主要是用它來作開發環境的封裝以利可㩦性,對於 Docker 用在 production 環境似乎還存有一些疑慮,可能是穩定度或效能之類的考量。但經過這幾年,好像不再會有人去質疑 Docker 到底適不適用於 procuction,而 Kubernetes 這類容器管理工具的興起,也強化了容器在生產環境運行的可用性。開場白結束,接下來就讓我們來研究研究 Docker 吧。
市面上有許多關於 Docker 的教學和書籍,但這次介紹還是會從官方文件為主,如果有引用到其他資料會再註明。我們從官方文件的 Get Started
開始,請參考 https://docs.docker.com/get-started/。我自己覺得 Docker 的 Get Started 寫得很好,它先簡單地介紹 Docker 的概念,然後安裝 Docker,並以一個應用程式作為範例,循序漸進地介紹 Docker 生態系的重要元件及工具。強烈建議想學 Docker 的朋友直接從這份 Get Started
入手。
Docker 是什麼呢?它是一個讓開發和維員人員利用容器 (container) 來開發 (develop)、部署 (deploy) 和運行 (run) 應用程式的平台。使用 Linux 容器來部署應用程式稱作 containerization,應該可以翻成「容器化」。容器化愈來愈受歡迎,它具有以下的優點:
有點不知道在翻譯什麼但沒關係,通常在介紹 Docker 之前,都要先說明「容器」和「虛擬機器」有什麼不同。容器以「原生」的方式在 Linux 上運行(文件這裡用的是 natively 這個字),和其他的容器共用 host 的核心 (kernel)。基本上每個容器就只是一個單獨的行程 (process)。而虛擬機器則是透過 hypervisor 去存取 host 資源,以運行一整個作業系統,通常會耗用較多的資源。我們可以從下面的圖示看出兩者的不同。
(圖片取自 https://docs.docker.com/get-started/#images-and-containers)
接下來就來安裝 Docker。Docker 目前有兩種版本,社群版本 (Docker CE,community edition) 和商用版本 (Docker EE,enterprise edition),我們安裝社群版本就可以了。一樣支援 Linux、Mac OS、Windows 等主流作業系統,這裡以 Ubuntu 為例,其他作業系統的詳細安裝方式,請參考官方文件 https://docs.docker.com/install/ 一節的說明。
這邊要稍微留意的,是在 macOS 和 Windows 中安裝。在這兩種作業系統中安裝 Docker 有兩種方式,一種是 Docker Toolbox,另一種是 Docker for Mac 或 Docker for Windows。因為 Docker 的 container 使用 Linux 核心,在 Mac 或 Windows 比較早期的方式是先安裝一個 Linux 虛擬機器,通常是用 VirtualBox
,然後用它來執行 Docker,在 Mac 或 Windows 上使用 docker 相關指令,其實是對那台虛擬機上的 Docker 進行操作。Docker for Mac 和 Docker for Windows 是在比較新的 macOS 或 Windows 版本上執行 Docker 的方式(不確定是否只要作業系統在某個版本以後,印象中還需要硬體支援)。它其實還是透過 hypervisor,在 Windows 是 Microsoft Hyper-V,macOS 則是 Mac HyperKit,但底層是利用作業系統自己的虛擬化技術來支援,感覺起來比較接近原本的 Docker,不需要再透過一台 VirtualBox 虛擬機來操作。目前 Docker Toolbox 已經比較少在用了,除非機器比較老舊,不支援 Docker for Mac / Windows,否則請先嘗試以 Docker for Mac / Windows 來安裝。
我們用官方推薦,也是最簡單的方式,透過 Docker 儲存庫來安裝 Docker CE。
$ sudo apt-get update
$ sudo apt-get install apt-transport-https ca-certificates curl
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce
安裝好之後,我們來看一下它的版本及基本訊息。
$ sudo docker --version
$ sudo docker info
接下來我們來執行 Docker 的 hello-world
。它會去下載 hello-world
這個映像檔並執行它。
$ sudo docker run hello-world
請注意在一般狀況下,執行 docker 相關指令是需要 root 權限的,所以在 docker run
前我們用了 sudo
,以下範例會省略 sudo
。
如果成功的話,會出現以下畫面:
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
前面提到下載 hello-world
映像檔,映像檔 (image) 和容器的關係是什麼呢?其實有點像是 Vagrant 裡 box 和虛擬機器的關係,啟動 (lauch) 映像檔,就可以得到一個容器,而映像檔其實就是一個打包好、可執行的套件,裡面包括應用服務執行時所需的所有東西,像是程式碼、運行程式庫、設置檔等等。容器是映像檔的一個運行實例 (runtime instance) ,此時映像檔會被載入記憶體,並成為一個行程 (process)。
知道了 image 和 container 的差異,我們來看看幾個命令,首是先查看下載了那些映像檔。
$ docker image ls
或者
$ docker images
結果如下:
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest 4ab4c602aa5e 7 weeks ago 1.84kB
查看目前系統中有那些容器:
$ docker container ls
或者
$ docker ps
不過這裡執行 docker container ls
不會看到剛才執行 hello-world
映像檔所產生的容器,因為該容器一執行後就會停止,而 docker container ls
不會列出停止的容器,必須加上 -a
(--all
) 旗標。執行 docker container ls -a
的結果如下:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e05b1d763e3f hello-world "/hello" 48 seconds ago Exited (0) 47 seconds ago competent_cori
我將 Docker 指令分為兩類,一類是管理類的指令,另一類是一般指令。管理類的指令用來管理 docker 物件,基本格式為 docker <object> <action>
,下面是物件管理的指令。
$ docker config
$ docker image
$ docker container
$ docker network
$ docker node
$ docker plugin
$ docker secret
$ docker service
$ docker stack
$ docker swarm
$ docker system
$ docker trust
$ docker volume
使用 docker --help
來查看可用的指令,每個指令皆可以 --help
旗標獲得更詳細的資訊。管理類指令在執行時通常會加上動作,例如 docker image ls
。其中某些操作會有另一個一般指令可以達到相同的效果,例如上面提到的兩個例子,dokcer image ls
和 docker images
、dokcer container ls
和 docker ps
等等。
Get Started
的第一部分到此為止,接下來會以一個簡單的應用程式作為範例,並依下列順序逐漸擴充:
文件中將這個應用程式的開發分為三個階層,最底層是容器,中間是服務,最上層是堆疊,接下來依序說明這三層,但首先介紹這個應用程式本身在做些什麼。
這個應用程式是用 Python 的 Flask 框架建立一個簡單的網頁伺服器,並且連接一個 Redis 資料庫(這個資料庫目前還沒有)。以前要建立這樣的應用程式,必須先安裝 Python,確保開發環境能讓程式碼如預期般的執行,而且和生產環境保持一致性。若使用 Docker 來開發,只要選擇一個有Python 運行的映像檔,並將它連同應用程式源碼一併打包即可,這個打包好的映像檔應該在開發環境、生產環境都可以執行,而且所有需要的相依檔案都包含在這個映像檔之中,就不會有那種「在我的電腦可以跑」的情況發生。那要怎麼把這些東西打包成一個映像檔呢?這時 Dockerfile 就派上用場了。下面是 Get Started
這一節所提供的 Dockerfile
。跟 Vagrant 的情況一樣,這個檔案本身就叫 Dockerfile
。
# Use an official Python runtime as a parent image
FROM python:2.7-slim
# Set the working directory to /app
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . /app
# Install any needed packages specified in requirements.txt
RUN pip install --trusted-host pypi.python.org -r requirements.txt
# Make port 80 available to the world outside this container
EXPOSE 80
# Define environment variable
ENV NAME World
# Run app.py when the container launches
CMD ["python", "app.py"]
我們透過註解來看一下這個 Dockerfile
在做什麼。首先是我們要打包的映像檔會用到的基礎映像檔,這裡用的是 python:2.7-slim
,接下來是設定工作目錄,大概就是進入這個容器後要 cd
(change directory) 到 /app
的意思。然後我們要把一些檔案拷貝到 /app
下,並使用 pip install
安裝一些所需的套件,並揭露 (expose) 容器的 port 80,不過實際上要連線到這個服務時還是要綁定本機的某個 port。最後是設定環境變數,並執行 app.py
這支程式。
接下來還有兩個需要的檔案,第一個是 requirements.txt
,記錄應用程式所需的 Python 套件:
Flask
Redis
接下來是應用程式本身,檔名為 app.py
from flask import Flask
from redis import Redis, RedisError
import os
import socket
# Connect to Redis
redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)
app = Flask(__name__)
@app.route("/")
def hello():
try:
visits = redis.incr("counter")
except RedisError:
visits = "<i>cannot connect to Redis, counter disabled</i>"
html = "<h3>Hello {name}!</h3>" \
"<b>Hostname:</b> {hostname}<br/>" \
"<b>Visits:</b> {visits}"
return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)
if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)
把 Dockerfile
、requirements.txt
、app.py
放在同一個目錄中,並進入該目錄,就可以來建立 (build) 所需的映像檔了。
$ docker build -t friendlyhello .
最後的 .
是指將所在的目錄作為 build context,build context 不知道中文怎麼翻比較好,但之後會介紹。-t
的旗標用來幫這個映像檔命名。這裡 t
其實是 tag 的意思,但它的參數 friendlyhello
比較類似是映像檔的名稱。建立映像檔後可以用前面介紹過的 docker images
去看有沒有建立成功,發現剛才 -t
參數 friendlyhello
出現在 REPOSITORY
這個欄位,TAG
欄位出現的反而是 latest
,這裡請稍微確認一下。
接下來執行下面這個指令,它會執行剛才建立的映像檔 friendlyhello
並產生一個容器。-p 4000:80
參數用來將容器的 80 port 對應到本機的 4000 port,這樣才能從外部連接到這個容器裡的應用程式。
$ docker run -p 4000:80 friendlyhello
輸出如下:
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:80/ (Press CTRL+C to quit)
這個容器目前是運作在前景 (foreground) 的。打開瀏覽器,在網址列輸入 http://localhost:4000
,沒問題的話就可以看到以下的畫面,表示應用程式已成功在容器中運行。
回到終端機,會看到畫面上多了剛才以瀏覽器讀取網站首頁的記錄。現在依提示鍵入 Ctrl - C
以停止容器執行。文件指出在 Windows 環境中這麼做並不會停止容器,因此要用 docker container stop
停止此容器,待會會介紹這個指令。
再執行一次應用程式,但是讓它以「分離模式」(detached mode) 在背景中執行。輸入以下指令,會輸出容器的 ID 並返回命令提示字元。
$ docker run -d -p 4000:80 friendlyhello
以 docker container ls
或 docker ps
確認目前執行中的容器,結果如下:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2c826b5a8e36 friendlyhello "python app.py" About a minute ago Up About a minute 0.0.0.0:4000->80/tcp agitated_heyrovsky
因為剛才 docker run
的指令沒有給予產生的容器名稱,Docker 會隨意給予此容器一個名稱,請參照 NAMES
欄位。現在以 docker container stop <container>
指令來停止此容器,最後的參數可輸入 container ID 或 name。以上述例子而言,container ID 是 2c826b5a8e36
,名稱則是 agitated_heyrovsky
,若使用 container ID,可使用前面若干字元,長度讓 Docker 能唯一辨識即可。例如:
$ docker container stop 2c82
或者
$ docker container stop agitated_heyrovsky
docker container stop
會優雅停止 (gracefully stop) 容器,若要強制關閉 (force shutdown) 容器,可用 docker container kill <container>
指令。
此外在列出 (ls
) 映像檔或容器的指令時,若使用 -q
(--quiet
) 旗標 ,只會列出物件 ID,可搭配 rm
或 kill
這類需要物件 ID 作為參數的指令使用,例如要刪除所有容器,可用這個指令:
$ docker container rm $(docker container ls -a -q)
因為 docker container rm
等於 docker rm
,docker container ls
等於 docker ps
,所以上面的指令也可以寫成
$ docker rm $(docker ps -aq)
請注意因為沒有加上 -f
旗標,所以不會刪除運作中的容器,要是有容器還在運行狀態中會跳出錯誤訊息,但其他已停止的容器仍會被刪除。
要查看某個物件的資訊,可以用 docker inspect <object>
指令。這裡的 object 可以是映像檔或容器的 ID。
今天介紹了 Docker 的基本概念、安裝方法,接下來跟著官方文件的 Get Started
,瞭解使用 Docker 作為應用程式服務可分為三種層級。在第一個 container 容器層級,利用 Dockerfile
建立了自己的映像檔,並在單個容器中執行應用程式。明天會先介紹如何分享建立好的映像檔,並繼續跟著文件進入 service 服務層級的實作。